package com.unitd.scheduler.job;

import com.unitd.frame.comm.utils.StringUtils;
import com.unitd.frame.spring.SpringInstanceFactory;
import com.unitd.scheduler.model.JobConfig;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;

import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @desc Job定时任务基类
 * @filename AbstractJob.java
 * @copyright www.unitd.com
 * @author Hudan
 * @version 1.0
 * @date 2016/10/16
 */
public abstract class AbstractJob implements DisposableBean {

	private static final Logger logger = LoggerFactory.getLogger(AbstractJob.class);

	/** 默认允许多个节点时间误差 */
	private static final long DEFAULT_ALLOW_DEVIATION = 1000 * 60 * 15;

	/** Job所在组名 */
	protected String group;
	/** Job名称 */
	protected String jobName;
	/** 定时器的执行时间表达式, 按照schedule的规则来定义(比如: 0/30 * * * * ?) */
	protected String cronExpr;
	/** 定时任务的触发器*/
	protected String triggerName;
	/** 定时任务的调度器 */
	private Scheduler scheduler;
	/** quartz的触发器配置对象 */
	private CronTriggerImpl cronTrigger;
	/** quartz的触发器键对象 */
	private TriggerKey triggerKey;
	/** 任务执行间隔(毫秒) */
	private long jobFireInterval = 0;
	/** 启动是否立即执行 */
	private boolean executeOnStarted;
	/** 失败重试次数 */
	private int retries = 0;
	/** 定时服务运行的次数 */
	private AtomicInteger runCount = new AtomicInteger(0);

	/**
	 * @desc 是否开启并行处理
	 * @return boolean
	 */
	public abstract boolean parallelEnabled();

	/**
	 * @desc 执行实际的定时任务业务逻辑
	 * @param context 定时器上下文对象
	 * @throws Exception
	 */
	public abstract void doJob(JobContext context) throws Exception;

	/**
	 * @desc 初始化定时任务管理器
	 */
	public void init() {

		// 拼装 定时任务的触发器名称(默认:Job名称 + Trigger)
		triggerName = jobName + "Trigger";
		// quartz的触发器键对象
		triggerKey = new TriggerKey(triggerName, group);
		// 定时器任务配置对象
		JobConfig jobConfg = new JobConfig(group, jobName, cronExpr);

		// 从持久化配置合并
		if (JobContext.getContext().getConfigPersistHandler() != null) {
			JobContext.getContext().getConfigPersistHandler().merge(jobConfg);
		}

		JobContext.getContext().getRegistry().register(jobConfg);
		logger.info("Initialized Job_{} OK!!", jobName);
	}

	/**
	 * @desc 在初始化定时任务加载器之后需要处理的操作
	 */
	public void afterInitialized() {
		// 启动重试任务
		if (retries > 0) {
			JobContext.getContext().startRetryProcessor();
		}
		// 这里不能提前去拿下一次更新时间，否则真正执行后下次执行时间不更新
//		if(executeOnStarted)return;
//		JobConfig conf = JobContext.getContext().getRegistry().getConf(jobName,false);
//		Date nextFireTime = getTrigger().getNextFireTime();
//		if(nextFireTime != null){
//			conf.setNextFireTime(nextFireTime);
//			JobContext.getContext().getRegistry().updateJobConfig(conf);
//		}
	}

	/**
	 * @desc 定时任务执行入口方法
	 */
	public void execute() {
		// 先获得定时任务初始化对象,避免InstanceFactory还未初始化,就执行
		SpringInstanceFactory.waitUtilInitialized();

		// 根据定时任务名称获取该定时任务的配置信息
		JobConfig schConf = JobContext.getContext().getRegistry().getConf(jobName, false);
		if (currentNodeIgnore(schConf))
			return;

		Date beginTime;
		Exception exception = null;
		try {
			// 更新状态
			beginTime = getPreviousFireTime();
			JobContext.getContext().getRegistry().setRuning(jobName, beginTime);
			logger.debug("Job_{} at node[{}] execute begin...", jobName, JobContext.getContext().getNodeId());
			// 执行任务
			doJob(JobContext.getContext());
			logger.debug("Job_{} at node[{}] execute finish", jobName, JobContext.getContext().getNodeId());
		} catch (Exception e) {
			// 重试任务
			if (retries > 0) JobContext.getContext().getRetryProcessor().submit(this, retries);
			logger.error("Job_" + jobName + " execute error", e);
			exception = e;
		}
		// 执行次数累加1
		runCount.incrementAndGet();
		Date nextFireTime = getTrigger().getNextFireTime();
		JobContext.getContext().getRegistry().setStoping(jobName, nextFireTime, exception!=null?exception.getLocalizedMessage():null);
		// 运行日志持久化
		doJobLogPersist(schConf, exception, nextFireTime);
		// 重置cronTrigger，重新获取才会更新previousFireTime，nextFireTime
		cronTrigger = null;
	}

	/**
	 * @desc 根据定时任务的时间表达式, 重置定时任务触发器
	 * @param newCronExpr 定时任务表达式
	 */
	public void resetTriggerCronExpr(String newCronExpr) {
		try {
			if (getTrigger() == null) return;
			String originConExpression = getTrigger().getCronExpression();
			// 判断任务时间是否更新过
			if (!originConExpression.equalsIgnoreCase(newCronExpr)) {
				getTrigger().setCronExpression(newCronExpr);
				getScheduler().rescheduleJob(triggerKey, getTrigger());
				getScheduler().resumeTrigger(triggerKey);
				logger.info("Job_{} CronExpression changed, origin:{},current:{}", jobName, originConExpression, newCronExpr);
			}
		} catch (Exception e) {
			logger.error("checkConExprChange error", e);
		}
	}

	/**
	 * @desc 根据定时任务配置对象,判断
	 * @param schConf 定时任务配置信息
	 * @return boolean
	 */
	protected boolean currentNodeIgnore(JobConfig schConf) {
		if (parallelEnabled()) return false;
		try {
			if (!schConf.isActive()) {
				logger.debug("Job_{} 已禁用,终止执行", jobName);
				return true;
			}

			// 执行间隔（秒）
			// long interval = getJobFireInterval();
			long currentTimes = Calendar.getInstance().getTime().getTime();

			if (schConf.getNextFireTime() != null) {
				// 下次执行时间 < 当前时间强制执行
				if (currentTimes - schConf.getNextFireTime().getTime() > DEFAULT_ALLOW_DEVIATION) {
					logger.debug("Job_{} NextFireTime[{}] before currentTime[{}],re-join-execute task ", jobName, currentTimes, schConf.getNextFireTime().getTime());
					return false;
				}
//				// 如果多个节点做了时间同步，那么误差应该为0才触发任务执行，但是考虑一些误差因素，可以做一个误差容错
//				if (schConf.getLastFireTime() != null) {
//					long deviation = Math.abs(currentTimes - schConf.getLastFireTime().getTime() - interval);
//					if (interval > 0 && deviation > DEFAULT_ALLOW_DEVIATION) {
//						logger.info("Job_{} interval:{},currentTimes:{},expect tiggertime:{}", jobName, interval, currentTimes, schConf.getLastFireTime().getTime());
//						return true;
//					}
//				}
			}

			// 如果执行节点不为空,且不等于当前节点
			if (StringUtils.isNotBlank(schConf.getCurrentNodeId())) {
				if (!JobContext.getContext().getNodeId().equals(schConf.getCurrentNodeId())) {
					logger.debug("Job_{} 指定执行节点:{}，不匹配当前节点:{}", jobName, schConf.getCurrentNodeId(), JobContext.getContext().getNodeId());
					return true;
				}
				// 如果分配了节点，则可以保证本节点不会重复执行则不需要判断runing状态
			} else {
				if (schConf.isRunning()) {
					// 如果某个节点开始了任务但是没有正常结束导致没有更新任务执行状态
					logger.info("Job_{} 其他节点[{}]正在执行,终止当前执行", schConf.getCurrentNodeId(), jobName);
					return true;
				}
			}

			this.cronExpr = schConf.getCronExpr();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * @desc 获取定时任务的触发器对象
	 * @return CronTriggerImpl
	 */
	private CronTriggerImpl getTrigger() {
		try {
			if (this.cronTrigger == null) {
				if (getScheduler() == null) return null;
				Trigger trigger = getScheduler().getTrigger(triggerKey);
				this.cronTrigger = (CronTriggerImpl) trigger;
			}
		} catch (SchedulerException e) {
			logger.error("Job_" + jobName + " Invoke getTrigger error", e);
		}
		return cronTrigger;
	}

	/**
	 * @desc 获取定时任务的下次调用时间
	 * @return Date
	 */
	private Date getPreviousFireTime() {
		return getTrigger().getPreviousFireTime() == null ? new Date() : getTrigger().getPreviousFireTime();
	}

	/**
	 * @desc 处理定时器任务的任务运行日志持久化
	 * @param schConf 定时器任务
	 * @param exception 定时任务运行时产生的异常信息
	 * @param nextFireTime 定时任务下次调用时间
	 */
	private void doJobLogPersist(JobConfig schConf, Exception exception, Date nextFireTime) {
		if (JobContext.getContext().getJobLogPersistHandler() != null) {
			try {
				if (exception == null) {
					JobContext.getContext().getJobLogPersistHandler().onSucess(schConf, nextFireTime);
				} else {
					JobContext.getContext().getJobLogPersistHandler().onError(schConf, nextFireTime, exception);
				}
			} catch (Exception e) {
				logger.warn("JobLogPersistHandler run error", e);
			}
		}
	}


	/**
	 * @desc 继承Spring的DisposableBean类的destroy方法,每次调用完成后,都会去自动调用此方法
	 */
	@Override
	public void destroy() throws Exception {
		JobContext.getContext().getRegistry().unregister(jobName);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((jobName == null) ? 0 : jobName.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		AbstractJob other = (AbstractJob) obj;
		if (jobName == null) {
			if (other.jobName != null)
				return false;
		} else if (!jobName.equals(other.jobName))
			return false;
		return true;
	}

	public String getGroup() {
		return group;
	}
	public void setGroup(String group) {
		this.group = StringUtils.trimToNull(group);
	}
	public String getJobName() {
		return jobName;
	}
	public void setJobName(String jobName) {
		this.jobName = StringUtils.trimToNull(jobName);
	}
	public String getCronExpr() {
		return cronExpr;
	}
	public void setCronExpr(String cronExpr) {
		this.cronExpr = StringUtils.trimToNull(cronExpr);
	}
	public String getTriggerName() {
		return triggerName;
	}
	public void setTriggerName(String triggerName) {
		this.triggerName = StringUtils.trimToNull(triggerName);
	}
	public Scheduler getScheduler() {
		if (scheduler == null)
			scheduler = SpringInstanceFactory.getInstance(Scheduler.class);
		return scheduler;
	}
	public void setScheduler(Scheduler scheduler) {
		this.scheduler = scheduler;
	}
	public TriggerKey getTriggerKey() {
		return triggerKey;
	}
	public void setTriggerKey(TriggerKey triggerKey) {
		this.triggerKey = triggerKey;
	}
	/**
	 * @desc 获取任务执行间隔
	 * @return 任务执行间隔(毫秒数)
	 */
	private long getJobFireInterval() {
		if (jobFireInterval == 0) {
			Date nextFireTime = getTrigger().getNextFireTime();
			Date previousFireTime = getTrigger().getPreviousFireTime();
			jobFireInterval = nextFireTime.getTime() - previousFireTime.getTime();
		}
		return jobFireInterval;
	}
	public void setJobFireInterval(long jobFireInterval) {
		this.jobFireInterval = jobFireInterval;
	}
	public boolean isExecuteOnStarted() {
		return executeOnStarted;
	}
	public void setExecuteOnStarted(boolean executeOnStarted) {
		this.executeOnStarted = executeOnStarted;
	}
	public int getRetries() {
		return retries;
	}
	public void setRetries(int retries) {
		this.retries = retries;
	}
	public AtomicInteger getRunCount() {
		return runCount;
	}
	public void setRunCount(AtomicInteger runCount) {
		this.runCount = runCount;
	}
}